// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

/* eslint-disable @typescript-eslint/no-use-before-define */
/* eslint-disable local-rules/dont-use-fspath */

import { Uri as URI } from 'vscode';
import { CharCode } from './charCode';
import * as extpath from './extpath';
import * as paths from './path';
import { isLinux, isWeb, isWindows } from './platform';
import { compare as strCompare, equalsIgnoreCase } from './strings';
import { Schemas } from './utils';

export function originalFSPath(uri: URI | undefined): string {
    return isWeb ? uri?.path || '' : uri?.fsPath || '';
}

//#region IExtUri

export interface IExtUri {
    // --- identity

    /**
     * Compares two uris.
     *
     * @param uri1 Uri
     * @param uri2 Uri
     * @param ignoreFragment Ignore the fragment (defaults to `false`)
     */
    compare(uri1: URI, uri2: URI, ignoreFragment?: boolean): number;

    /**
     * Tests whether two uris are equal
     *
     * @param uri1 Uri
     * @param uri2 Uri
     * @param ignoreFragment Ignore the fragment (defaults to `false`)
     */
    isEqual(uri1: URI | undefined, uri2: URI | undefined, ignoreFragment?: boolean): boolean;

    /**
     * Tests whether a `candidate` URI is a parent or equal of a given `base` URI.
     *
     * @param base A uri which is "longer" or at least same length as `parentCandidate`
     * @param parentCandidate A uri which is "shorter" or up to same length as `base`
     * @param ignoreFragment Ignore the fragment (defaults to `false`)
     */
    isEqualOrParent(base: URI, parentCandidate: URI, ignoreFragment?: boolean): boolean;

    /**
     * Creates a key from a resource URI to be used to resource comparison and for resource maps.
     * @see {@link ResourceMap}
     * @param uri Uri
     * @param ignoreFragment Ignore the fragment (defaults to `false`)
     */
    getComparisonKey(uri: URI, ignoreFragment?: boolean): string;

    /**
     * Whether the casing of the path-component of the uri should be ignored.
     */
    ignorePathCasing(uri: URI): boolean;

    // --- path math

    basenameOrAuthority(resource: URI): string;

    /**
     * Returns the basename of the path component of an uri.
     * @param resource
     */
    basename(resource: URI): string;

    /**
     * Returns the extension of the path component of an uri.
     * @param resource
     */
    extname(resource: URI): string;
    /**
     * Return a URI representing the directory of a URI path.
     *
     * @param resource The input URI.
     * @returns The URI representing the directory of the input URI.
     */
    dirname(resource: URI): URI;
    /**
     * Join a URI path with path fragments and normalizes the resulting path.
     *
     * @param resource The input URI.
     * @param pathFragment The path fragment to add to the URI path.
     * @returns The resulting URI.
     */
    joinPath(resource: URI, ...pathFragment: string[]): URI;
    /**
     * Normalizes the path part of a URI: Resolves `.` and `..` elements with directory names.
     *
     * @param resource The URI to normalize the path.
     * @returns The URI with the normalized path.
     */
    normalizePath(resource: URI): URI;
    /**
     *
     * @param from
     * @param to
     */
    relativePath(from: URI, to: URI): string | undefined;
    /**
     * Resolves an absolute or relative path against a base URI.
     * The path can be relative or absolute posix or a Windows path
     */
    resolvePath(base: URI, path: string): URI;

    // --- misc

    /**
     * Returns true if the URI path is absolute.
     */
    isAbsolutePath(resource: URI): boolean;
    /**
     * Tests whether the two authorities are the same
     */
    isEqualAuthority(a1: string, a2: string): boolean;
    /**
     * Returns true if the URI path has a trailing path separator
     */
    hasTrailingPathSeparator(resource: URI, sep?: string): boolean;
    /**
     * Removes a trailing path separator, if there's one.
     * Important: Doesn't remove the first slash, it would make the URI invalid
     */
    removeTrailingPathSeparator(resource: URI, sep?: string): URI;
    /**
     * Adds a trailing path separator to the URI if there isn't one already.
     * For example, c:\ would be unchanged, but c:\users would become c:\users\
     */
    addTrailingPathSeparator(resource: URI, sep?: string): URI;
}

export class ExtUri implements IExtUri {
    constructor(private _ignorePathCasing: (uri: URI) => boolean) {}

    compare(uri1: URI, uri2: URI, ignoreFragment: boolean = false): number {
        if (uri1 === uri2) {
            return 0;
        }
        return strCompare(this.getComparisonKey(uri1, ignoreFragment), this.getComparisonKey(uri2, ignoreFragment));
    }

    isEqual(uri1: URI | undefined, uri2: URI | undefined, ignoreFragment: boolean = false): boolean {
        if (uri1 === uri2) {
            return true;
        }
        if (!uri1 || !uri2) {
            return false;
        }
        return this.getComparisonKey(uri1, ignoreFragment) === this.getComparisonKey(uri2, ignoreFragment);
    }

    getComparisonKey(uri: URI, ignoreFragment: boolean = false): string {
        return uri
            .with({
                path: this._ignorePathCasing(uri) ? uri.path.toLowerCase() : undefined,
                fragment: ignoreFragment ? undefined : undefined
            })
            .toString();
    }

    ignorePathCasing(uri: URI): boolean {
        return this._ignorePathCasing(uri);
    }

    isEqualOrParent(
        base: URI,
        parentCandidate: URI,
        ignoreFragment: boolean = false,
        sep: '\\' | '/' = isWindows ? '\\' : '/'
    ): boolean {
        if (base.scheme === parentCandidate.scheme) {
            if (base.scheme === Schemas.file) {
                return (
                    extpath.isEqualOrParent(
                        originalFSPath(base),
                        originalFSPath(parentCandidate),
                        this._ignorePathCasing(base),
                        sep
                    ) &&
                    base.query === parentCandidate.query &&
                    (ignoreFragment || base.fragment === parentCandidate.fragment)
                );
            }
            if (isEqualAuthority(base.authority, parentCandidate.authority)) {
                return (
                    extpath.isEqualOrParent(base.path, parentCandidate.path, this._ignorePathCasing(base), '/') &&
                    base.query === parentCandidate.query &&
                    (ignoreFragment || base.fragment === parentCandidate.fragment)
                );
            }
        }
        return false;
    }

    // --- path math

    joinPath(resource: URI, ...pathFragment: string[]): URI {
        return URI.joinPath(resource, ...pathFragment);
    }

    basenameOrAuthority(resource: URI): string {
        return basename(resource) || resource.authority;
    }

    basename(resource: URI, ext?: string): string {
        return paths.posix.basename(resource.path, ext);
    }

    extname(resource: URI): string {
        return paths.posix.extname(resource.path);
    }

    dirname(resource: URI): URI {
        if (resource.path.length === 0) {
            return resource;
        }
        let dirname;
        if (resource.scheme === Schemas.file) {
            dirname = URI.file(paths.dirname(originalFSPath(resource))).path;
        } else {
            dirname = paths.posix.dirname(resource.path);
            if (resource.authority && dirname.length && dirname.charCodeAt(0) !== CharCode.Slash) {
                console.error(`dirname("${resource.toString})) resulted in a relative path`);
                dirname = '/'; // If a URI contains an authority component, then the path component must either be empty or begin with a CharCode.Slash ("/") character
            }
        }
        return resource.with({
            path: dirname
        });
    }

    normalizePath(resource: URI): URI {
        if (!resource.path.length) {
            return resource;
        }
        let normalizedPath: string;
        if (resource.scheme === Schemas.file) {
            normalizedPath = URI.file(paths.normalize(originalFSPath(resource))).path;
        } else {
            normalizedPath = paths.posix.normalize(resource.path);
        }
        return resource.with({
            path: normalizedPath
        });
    }

    relativePath(from: URI, to: URI): string | undefined {
        if (from.scheme !== to.scheme || !isEqualAuthority(from.authority, to.authority)) {
            return undefined;
        }
        if (from.scheme === Schemas.file) {
            const relativePath = paths.relative(originalFSPath(from), originalFSPath(to));
            return isWindows ? extpath.toSlashes(relativePath) : relativePath;
        }
        let fromPath = from.path || '/',
            toPath = to.path || '/';
        if (this._ignorePathCasing(from)) {
            // make casing of fromPath match toPath
            let i = 0;
            for (const len = Math.min(fromPath.length, toPath.length); i < len; i++) {
                if (fromPath.charCodeAt(i) !== toPath.charCodeAt(i)) {
                    if (fromPath.charAt(i).toLowerCase() !== toPath.charAt(i).toLowerCase()) {
                        break;
                    }
                }
            }
            fromPath = toPath.substr(0, i) + fromPath.substr(i);
        }
        return paths.posix.relative(fromPath, toPath);
    }

    resolvePath(base: URI, path: string): URI {
        if (base.scheme === Schemas.file) {
            const newURI = URI.file(paths.resolve(originalFSPath(base), path));
            return base.with({
                authority: newURI.authority,
                path: newURI.path
            });
        }
        path = extpath.toPosixPath(path); // we allow path to be a windows path
        return base.with({
            path: paths.posix.resolve(base.path, path)
        });
    }

    // --- misc

    isAbsolutePath(resource: URI): boolean {
        return !!resource.path && resource.path[0] === '/';
    }

    isEqualAuthority(a1: string | undefined, a2: string | undefined) {
        return a1 === a2 || (a1 !== undefined && a2 !== undefined && equalsIgnoreCase(a1, a2));
    }

    hasTrailingPathSeparator(resource: URI, sep: string = paths.sep): boolean {
        if (resource.scheme === Schemas.file) {
            const fsp = originalFSPath(resource);
            return fsp.length > extpath.getRoot(fsp).length && fsp[fsp.length - 1] === sep;
        } else {
            const p = resource.path;
            return (
                p.length > 1 &&
                p.charCodeAt(p.length - 1) === CharCode.Slash &&
                !/^[a-zA-Z]:(\/$|\\$)/.test(resource.fsPath)
            ); // ignore the slash at offset 0
        }
    }

    removeTrailingPathSeparator(resource: URI, sep: string = paths.sep): URI {
        // Make sure that the path isn't a drive letter. A trailing separator there is not removable.
        if (hasTrailingPathSeparator(resource, sep)) {
            return resource.with({ path: resource.path.substr(0, resource.path.length - 1) });
        }
        return resource;
    }

    addTrailingPathSeparator(resource: URI, sep: string = paths.sep): URI {
        let isRootSep: boolean = false;
        if (resource.scheme === Schemas.file) {
            const fsp = originalFSPath(resource);
            isRootSep = fsp !== undefined && fsp.length === extpath.getRoot(fsp).length && fsp[fsp.length - 1] === sep;
        } else {
            sep = '/';
            const p = resource.path;
            isRootSep = p.length === 1 && p.charCodeAt(p.length - 1) === CharCode.Slash;
        }
        if (!isRootSep && !hasTrailingPathSeparator(resource, sep)) {
            return resource.with({ path: resource.path + '/' });
        }
        return resource;
    }
}

/**
 * Unbiased utility that takes uris "as they are". This means it can be interchanged with
 * uri#toString() usages. The following is true
 * ```
 * assertEqual(aUri.toString() === bUri.toString(), exturi.isEqual(aUri, bUri))
 * ```
 */
export const extUri = new ExtUri(() => false);

/**
 * BIASED utility that _mostly_ ignored the case of urs paths. ONLY use this util if you
 * understand what you are doing.
 *
 * This utility is INCOMPATIBLE with `uri.toString()`-usages and both CANNOT be used interchanged.
 *
 * When dealing with uris from files or documents, `extUri` (the unbiased friend)is sufficient
 * because those uris come from a "trustworthy source". When creating unknown uris it's always
 * better to use `IUriIdentityService` which exposes an `IExtUri`-instance which knows when path
 * casing matters.
 */
export const extUriBiasedIgnorePathCase = new ExtUri((uri) => {
    // A file scheme resource is in the same platform as code, so ignore case for non linux platforms
    // Resource can be from another platform. Lowering the case as an hack. Should come from File system provider
    return uri.scheme === Schemas.file ? !isLinux : true;
});

/**
 * BIASED utility that always ignores the casing of uris paths. ONLY use this util if you
 * understand what you are doing.
 *
 * This utility is INCOMPATIBLE with `uri.toString()`-usages and both CANNOT be used interchanged.
 *
 * When dealing with uris from files or documents, `extUri` (the unbiased friend)is sufficient
 * because those uris come from a "trustworthy source". When creating unknown uris it's always
 * better to use `IUriIdentityService` which exposes an `IExtUri`-instance which knows when path
 * casing matters.
 */
export const extUriIgnorePathCase = new ExtUri((_) => true);

export const isEqual = extUri.isEqual.bind(extUri);
export const isEqualOrParent = extUri.isEqualOrParent.bind(extUri);
export const getComparisonKey = extUri.getComparisonKey.bind(extUri);
export const basenameOrAuthority = extUri.basenameOrAuthority.bind(extUri);
export const basename = extUri.basename.bind(extUri);
export const extname = extUri.extname.bind(extUri);
export const dirname = extUri.dirname.bind(extUri);
export const joinPath = extUri.joinPath.bind(extUri);
export const normalizePath = extUri.normalizePath.bind(extUri);
export const relativePath = extUri.relativePath.bind(extUri);
export const resolvePath = extUri.resolvePath.bind(extUri);
export const isAbsolutePath = extUri.isAbsolutePath.bind(extUri);
export const isEqualAuthority = extUri.isEqualAuthority.bind(extUri);
export const hasTrailingPathSeparator = extUri.hasTrailingPathSeparator.bind(extUri);
export const removeTrailingPathSeparator = extUri.removeTrailingPathSeparator.bind(extUri);
export const addTrailingPathSeparator = extUri.addTrailingPathSeparator.bind(extUri);

//#endregion

/**
 * Data URI related helpers.
 */
export namespace DataUri {
    export const META_DATA_LABEL = 'label';
    export const META_DATA_DESCRIPTION = 'description';
    export const META_DATA_SIZE = 'size';
    export const META_DATA_MIME = 'mime';

    export function parseMetaData(dataUri: URI): Map<string, string> {
        const metadata = new Map<string, string>();

        // Given a URI of:  data:image/png;size:2313;label:SomeLabel;description:SomeDescription;base64,77+9UE5...
        // the metadata is: size:2313;label:SomeLabel;description:SomeDescription
        const meta = dataUri.path.substring(dataUri.path.indexOf(';') + 1, dataUri.path.lastIndexOf(';'));
        meta.split(';').forEach((property) => {
            const [key, value] = property.split(':');
            if (key && value) {
                metadata.set(key, value);
            }
        });

        // Given a URI of:  data:image/png;size:2313;label:SomeLabel;description:SomeDescription;base64,77+9UE5...
        // the mime is: image/png
        const mime = dataUri.path.substring(0, dataUri.path.indexOf(';'));
        if (mime) {
            metadata.set(META_DATA_MIME, mime);
        }

        return metadata;
    }
}
